[OvirtEngine]锁机制
EngineLock
OvirtEngine是通过 LockManager 接口,提供对 command 执行时对实际进行资源的加解锁,确保并发执行时的安全性。根据应用的特点,提供了两种类型的锁( 独占锁 和 共享锁 ),封装在了 EngineLock 对象中。
private Map<String, Pair<String, String>> exclusiveLocks;
private Map<String, Pair<String, String>> sharedLocks;
public EngineLock() {
}
public EngineLock(Map<String, Pair<String, String>> exclusiveLocks, Map<String, Pair<String, String>> sharedLocks) {
this.exclusiveLocks = exclusiveLocks;
this.sharedLocks = sharedLocks;
}
独占锁 和 共享锁 都可以同时锁定多个资源,采用了 Map
资源组的类型如下:
| 序号 | 可锁定资源组类型名称 | 描述 |
|---|---|---|
| 1 | POOL | 池 |
| 2 | VDS | 主机 |
| 3 | VDS_INIT | 主机初始化 |
| 4 | VDS_FENCE | 电源管理 |
| 5 | VM | 虚拟机 |
| 6 | TEMPLATE | 模版 |
| 7 | DISK | 磁盘 |
| 8 | VM_DISK_BOOT | 引导盘 |
| 9 | VM_NAME | 虚拟机名称 |
| 10 | NETWORK | 网络 |
| 11 | STORAGE | 存储 |
| 12 | STORAGE_CONNECTION | 存储连接 |
| 13 | REGISTER_VDS | 注册主机 |
| 14 | VM_SNAPSHOTS | 虚拟机快照 |
| 15 | GLUSTER | 卷 |
| 16 | USER_VM_POOL | 池分配虚拟机给用户 |
| 17 | REMOTE_TEMPLATE | 导出模版 |
| 18 | REMOTE_VM | 导出虚拟机 |
在AbstractCommand中对获得独占锁方法getExclusiveLocks()和获得共享方法 getSharedLocks()都给出了默认的空实现,子类可以重写这两个方法来添加锁定的资源。
锁资源竞争过程
在Ovirt的command类执行真的逻辑之前会对command的执行的条件进行初始化和校验工作,其中就涉及了对线程共享资源的限制,在执行命令前会尝试去获得锁,该方法在AbstractCommand中的实现如下:
public boolean acquireLock() {
//从parameters中获得锁属性,如果parameters没有相应的参数,则使用默认值
//默认值包括锁范围为none,当锁竞争失败时不等待获取锁
//可以通过重写applyLockProperties方法对锁的属性进行修改
LockProperties lockProperties = getLockProperties();
boolean result = true;
//如果锁范围为none则不去尝试获得锁,视为获得锁成功
if (!Scope.None.equals(lockProperties.getScope())) {
//如果锁的范围为Execution,将releaseLocksAtEndOfExecute设为true
//releaseLocksAtEndOfExecute表示在command的Execute阶段结束释放锁
//command的多个阶段在这里不进行赘述
releaseLocksAtEndOfExecute = Scope.Execution.equals(lockProperties.getScope());
//判断是否等待获得锁,默认值为false
if (!lockProperties.isWait()) {
//由于默认值的原因,ovirt中大部分逻辑发生在这里
//当发生锁竞争时,放弃获得锁,command执行失败
result = acquireLockInternal();
} else {
//当发生锁竞争时,等待获得锁,然后继续执行commnad
acquireLockAndWait();
}
}
return result;
}
由于acquireLockAndWait()方法与acquireLockInternal()方法逻辑上的最主要的区别只有锁竞争失败时是否继续等待获得锁(共享锁不需要等待),所以这里就只对主要执行逻辑acquireLockInternal()方法进行展开。
protected boolean acquireLockInternal() {
//如果当前的command没有锁,则创建一个新锁,否则很可能已经获得了父命令的锁。
if (context.getLock() == null) {
//没有锁则构建EngineLock
EngineLock lock = buildLock();
//如果构建的锁不为空才继续尝试获得锁
//即如果子类没有重写getExclusiveLocks()或者getSharedLocks()来添加锁,则视为获得锁成功
if (lock != null) {
//通过LockManager来尝试获得锁
Pair<Boolean, Set<String>> lockAcquireResult = getLockManager().acquireLock(lock);
if (lockAcquireResult.getFirst()) {
log.info("Lock Acquired to object '{}'", lock);
//获得锁成功后将context锁设置为构造的锁
context.withLock(lock);
} else {
log.info("Failed to Acquire Lock to object '{}'", lock);
getReturnValue().getValidationMessages()
.addAll(extractVariableDeclarations(lockAcquireResult.getSecond()));
return false;
}
}
}
return true;
}
private EngineLock buildLock() {
EngineLock lock = null;
//这个两个方法默认返回值为null,由子类重写来加锁
Map<String, Pair<String, String>> exclusiveLocks = getExclusiveLocks();
Map<String, Pair<String, String>> sharedLocks = getSharedLocks();
//如果子类没有重写其中任何一个方法,则不进行锁的构建
if (exclusiveLocks != null || sharedLocks != null) {
lock = new EngineLock(exclusiveLocks, sharedLocks);
}
return lock;
}
从上面的代码可以看,出如果当前的command拥有锁,则说明使用父command的锁;如果当前command没有锁,则构建新的锁,锁的构建与子类是否重写了getExclusiveLocks()或者getSharedLocks()来添加锁有关,若没重写任何方法来添加锁则视为获得锁成功,否则调用LockManager的acquireLock方法来尝试获得锁。
public Pair<Boolean, Set<String>> acquireLock(EngineLock lock) {
log.debug("Before acquiring lock '{}'", lock);
//这里的globalLock为JUC的ReentrantLock对象,是一个全局锁
//之所以称为全局锁,是因为所有运行的command调用此方法时都会等待获得globalLock,然后再执行后续操作
//锁的粒度比较大,因此称为全局锁
globalLock.lock();
try {
//获得了锁的command才能够执行acquireLockInternal方法
return acquireLockInternal(lock);
} finally {
globalLock.unlock();
}
}
这里使用了JUC的ReentrantLock来实现同步,保证多个commnad线程执行此方法时,只有一个线程在执行acquireLockInternal()方法。
private Pair<Boolean, Set<String>> acquireLockInternal(EngineLock lock) {
boolean checkOnly = true;
//这里执行了两次循环,第一次checkOnly为true,第二次checkOnly为false
//即第一次只校验是否能够插入锁,第二次才去插入锁
//这样做的目的是为了避免如果校验和插入同时进行,如果后续校验失败需要对之前插入的锁进行删除
for (int i = 0; i < 2; i++) {
if (lock.getSharedLocks() != null) {
for (Entry<String, Pair<String, String>> entry : lock.getSharedLocks().entrySet()) {
//将共享锁放入hashMap缓存,并返回是否获成功获得锁
Pair<Boolean, Set<String>> result =
insertSharedLock(buildHashMapKey(entry),
entry.getValue().getSecond(), checkOnly);
if (!result.getFirst()) {
log.debug("Failed to acquire lock. Shared lock is taken for key '{}'," +
"value '{}'",
entry.getKey(),
entry.getValue().getFirst());
return result;
}
}
}
if (lock.getExclusiveLocks() != null) {
for (Entry<String, Pair<String, String>> entry : lock.getExclusiveLocks().entrySet()) {
Pair<Boolean, Set<String>> result =
//将独占锁放入hashMap缓存,并返回是否获成功获得锁
insertExclusiveLock(buildHashMapKey(entry),
entry.getValue().getSecond(), checkOnly);
if (!result.getFirst()) {
log.debug("Failed to acquire lock. Exclusive lock is taken for key '{}'," +
"value '{}'",
entry.getKey(),
entry.getValue().getFirst());
return result;
}
}
}
checkOnly = false;
}
log.debug("Success acquiring lock '{}' succeeded ", lock);
return LOCK_INSERT_SUCCESS_RESULT;
}
上面的代码使用了两次循环来执行锁插入操作(第一次只检查能否插入,第二才真正进行插入),目的是当有多个锁需要检查的话,避免了后续检查失败时要对之前插入的锁进行删除。而insertSharedLock()和insertExclusiveLock()真正获得锁的方法,而锁获得的逻辑十分简单,使用MapEntry的key以及Pair的第一个值构成hashMap的的key,Pair的第二值作为value存入HashMap缓存。区别两者对于获得锁成功的判断不同:
- 独占锁:如果对应的key在HashMap中不存在,将key和value存入HashMap,视为获得锁成功。如果对应的key在HashMap中存在,视为获得锁失败。
- 共享锁:如果对应的key在HashMap中不存在,将key和value存入HashMap,视为获得锁成功。如果对应的key在HashMap中存在,判断锁对象是不是独占锁,如果是独占锁,视为获得锁失败,否则将共享锁的count数加一。
到此,锁竞争的逻辑就结束了,当获得锁失败后,命令的校验也就失败了,命令也就执行失败了。
锁的释放
锁的释放在AbstractCommand的freeLock()方法中实现,如下:
public void freeLock() {
// 如果context锁不为空,进行释放锁的工作
if (context.getLock() != null) {
// 调用LockManager的releaseLock方法释放当前锁
getLockManager().releaseLock(context.getLock());
log.info("Lock freed to object '{}'", context.getLock());
context.withLock(null);
// 释放其他锁自定义的锁
freeCustomLocks();
}
}
freeLock方法会在以下几种情况下被调用:
- command的校验工作失败,最终会释放校验过程中加的锁
- 锁的范围为Execute,在command的Execute阶段结束后释放锁
- command的所有action结束,在endAction方法中释放锁
- 在command中主动调用freeLock方法
LockManager的releaseLock方法如下:
public void releaseLock(EngineLock lock) {
log.debug("Before releasing a lock '{}'", lock);
//使用全局锁
globalLock.lock();
try {
if (lock.getSharedLocks() != null) {
for (Entry<String, Pair<String, String>> entry : lock.getSharedLocks().entrySet()) {
//释放共享锁
releaseSharedLock(buildHashMapKey(entry), entry.getValue().getSecond());
}
}
if (lock.getExclusiveLocks() != null) {
for (Entry<String, Pair<String, String>> entry : lock.getExclusiveLocks().entrySet()) {
//释放独占锁
releaseExclusiveLock(buildHashMapKey(entry));
}
}
//isAwait为true的锁,会在竞争锁失败时挂起
//这里唤醒等待独占锁的线程开始竞争获得锁
releasedLock.signalAll();
} finally {
globalLock.unlock();
}
}
释放锁的过程也很简单,共享锁释放时将count减1,当count为0时从缓存中删除锁;而独占锁释放时直接将锁从缓存中删除即可。
只改变数据库状态的操作锁
OvirtEngine中还存在另一种锁,只是改变Entity在数据库的状态,从而实现限制前台页面操作的功能。例如,前台锁定VM的机制,实际上是将数据库的status字段设置为ImageLocked状态,而前台会根据这个状态来限制页面的操作功能。VmHandler中lockVm方法如下:
public static void lockVm(final VmDynamic vm, final CompensationContext compensationContext) {
TransactionSupport.executeInNewTransaction(() -> {
// 在修改状态前,先存储vm的快照信息,用于操作失败时恢复数据
compensationContext.snapshotEntityStatus(vm);
// 调用lockVm方法修改vm状态,主要就是操作数据库修改status字段
lockVm(vm.getId());
compensationContext.stateChanged();
});
}
public static void lockVm(Guid vmId) {
Backend.getInstance()
.getResourceManager()
.runVdsCommand(VDSCommandType.SetVmStatus,
new SetVmStatusVDSCommandParameters(vmId, VMStatus.ImageLocked));
}
是否需要加锁,由代码的逻辑决定;而解锁工作一般在command执行成功或command失败时执行。
前台如何实现锁定操作?
在VmListModel的updateActionsAvailability()方法维护着前台按钮是否可以点击的逻辑,例如删除虚拟机按钮的可执行逻辑为:
getRemoveCommand()
.setExecutable(vmsSelected && VdcActionUtils.canExecutePartially(items,
VmWithStatusForExclusiveLock.class, VdcActionType.RemoveVm));
可以看出是否能点击的主要逻辑在于VdcActionUtils.canExecutePartially()方法
public static boolean canExecutePartially(List<? extends BusinessEntityWithStatus<?, ?>> entities, Class type, VdcActionType action) {
if (blacklist.containsKey(type)) {
for (BusinessEntityWithStatus<?, ?> a : entities) {
if (a.getClass() == type && (!blacklist.get(type).containsKey(a.getStatus())
|| !blacklist.get(type).get(a.getStatus()).contains(action))) {
return true;
}
}
} else {
return true;
}
return false;
}
可以看出前台维护着一张status与Action之间的黑名单列表,即某些状态下一些Action不能执行,VMStatus.ImageLocked的action黑名单如下:
vmBlacklist.put(
VMStatus.ImageLocked,
EnumSet.of(
VdcActionType.RunVm,
VdcActionType.CloneVm,
VdcActionType.RunVmOnce,
VdcActionType.StopVm,
VdcActionType.ShutdownVm,
VdcActionType.HibernateVm,
VdcActionType.MigrateVm,
VdcActionType.RemoveVm,
VdcActionType.AddVmTemplate,
VdcActionType.ExportVm,
VdcActionType.ImportVm,
VdcActionType.ChangeDisk,
VdcActionType.CreateAllSnapshotsFromVm,
VdcActionType.AddVmInterface,
VdcActionType.UpdateVmInterface,
VdcActionType.RemoveVmInterface,
VdcActionType.CancelMigrateVm,
VdcActionType.ExtendImageSize,
VdcActionType.RebootVm));